// Copyright (c) <2010> <Playdead>
// This file is subject to the MIT License as seen in the trunk of this repository
// Maintained by: <Kristian Kjems> <kristian.kjems+UnitySVN@gmail.com>

// This script uses the CommandLine wrapper for a console version of 'svn'.
// All version of svn using 'svn status -v -u --xml' returns an identical XML formated
// document containing the current state of the repository. The XML document is parsed
// and all information is stored in a hashtable with assetpath as key and a class of
// SVN state enums as value. The hashtable work as a SVNStatus database. The 
// SVNStatus database is private and not directly accesible. Any component interrested
// should subscribe to SVNDatabaseHandler event to receive a copy of the database.
// A copy is made for isolation to avoid unnecessary synchronization of shared state.

#define SVN_ENABLED
#if (SVN_ENABLED)
using System;
using UnityEngine;
using UnityEditor;
using System.Xml;
using System.Linq;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using System.Threading;
using CommandLineExecution;

#pragma warning disable 0219,0414


namespace SVNIntegrationNameSpace
{

    #region SVNEnums
    public enum ESVNFileStatus
    {
        Normal,
        Added,
        Conflicted,
        Deleted,
        Ignored,
        Modified,
        Replaced,
        Unversioned,
        Missing,
    }

    public enum ESVNDirectoryStatus
    {
        NoModification,
        Conflicted,
        Modified
    }

    public enum ESVNRepositoryStatus // Locked status. If Locked then clean up needs to be executed
    {
        NotLocked,
        Locked
    }

    public enum ESVNScheduleStatus
    {
        None,
        Commit
    }

    public enum ESVNLockStatus
    {
        NoLock,
        LockedHere,
        LockedOther,
        LockedButStolen,
        NoLockButHasToken
    }

    public enum ESVNTreeConflictStatus
    {
        Normal,
        TreeConflict
    }

    public enum ESVNProperty
    {
        None,
        Normal,
    }

    public enum ESVNReflectionLevel
    {
        None,
        Local,
        Repository,
    }

    #endregion

    #region EventArgs
    public abstract class SVNDatabaseArgs : System.EventArgs
    {
    }

    public class SVNDatabaseUpdatedArgs : SVNDatabaseArgs
    {
        public SVNDatabaseUpdatedArgs(CommandLineOutput output, Dictionary<string, SVNStatus> statusDatabase)
        {
            this.statusDatabase = statusDatabase;
            this.output = output;
        }
        public readonly Dictionary<string, SVNStatus> statusDatabase;
        public readonly CommandLineOutput output;
    }

    public class SVNDatabaseBusyArgs : SVNDatabaseArgs
    {
        public SVNDatabaseBusyArgs(ICommandLineOperation commandlineOperation)
        {
            this.commandlineOperation = commandlineOperation;
        }
        public readonly ICommandLineOperation commandlineOperation;
    }

    public class SVNDatabaseErrorArgs : SVNDatabaseArgs
    {
        public SVNDatabaseErrorArgs(CommandLineOutput output)
        {
            this.output = output;
        }
        public readonly CommandLineOutput output;
    }
    #endregion

    public class SVNStatus
    {
        public ESVNReflectionLevel reflectionLevel;
        public ESVNFileStatus fileStatus;
        public ESVNDirectoryStatus directoryStatus;
        public ESVNRepositoryStatus repositoryStatus;
        public ESVNScheduleStatus scheduleStatus;
        public ESVNLockStatus lockStatus;
        public ESVNProperty property;
        public ESVNTreeConflictStatus treeConflictStatus;
        public string user;
        public string owner;
        public int revision;
        public int lastModifiedRevision;
        public bool bypassRevisionControl = false;
    }

    public sealed class SVNIntegration
    {
        SVNIntegration()
        {
            EditorApplication.update += new EditorApplication.CallbackFunction(EditorUpdate);
            asyncSvnStatusOperation = new AsyncCommandLineOperation(new CommandLine("svn", "status --non-interactive --xml -u -v", Application.dataPath), (object source, CommandLineCompletedEventArgs e) =>
            {                
                XmlDocument xmlResult = new XmlDocument();
                xmlResult.LoadXml(e.output.outputStr);
                svnStatusDatabase.Clear();
                ParseStatusResult(xmlResult);
                SyncedToUpdateOperation syncOp = new SyncedToUpdateOperation((object context) =>
                {
                    if (e.output.failed)
                    {
                        SendSVNErrorEvent(e.output);
                    }
                    AssetDatabase.Refresh();
                    OnDatabaseHandlerEvent(new SVNDatabaseUpdatedArgs(e.output, new Dictionary<string, SVNStatus>(svnStatusDatabase)));
                }, null);
                syncOp.Start();
            });

            syncSvnStatusOperation = new SyncCommandLineOperation(new CommandLine("svn", "status --non-interactive --xml -u -v", Application.dataPath), (object source, CommandLineCompletedEventArgs e) =>
            {                
                XmlDocument xmlResult = new XmlDocument();
                xmlResult.LoadXml(e.output.outputStr);
                svnStatusDatabase.Clear();
                ParseStatusResult(xmlResult);
                if (e.output.failed)
                {
                    SendSVNErrorEvent(e.output);
                }
                AssetDatabase.Refresh();
                OnDatabaseHandlerEvent(new SVNDatabaseUpdatedArgs(e.output, new Dictionary<string, SVNStatus>(svnStatusDatabase)));
            });

            syncLocalSvnStatusOperation = new SyncCommandLineOperation(new CommandLine("svn", "status --non-interactive --xml", Application.dataPath), (object source, CommandLineCompletedEventArgs e) =>
            {
                if (e.output.failed)
                {
                    SendSVNErrorEvent(e.output);
                }
                XmlDocument xmlResult = new XmlDocument();
                xmlResult.LoadXml(e.output.outputStr);
                ParseStatusResult(xmlResult);
                OnDatabaseHandlerEvent(new SVNDatabaseUpdatedArgs(e.output, new Dictionary<string, SVNStatus>(svnStatusDatabase)));
            });
        }
        static readonly SVNIntegration mInstance = new SVNIntegration();
        public static SVNIntegration instance { get { return mInstance; } }


        #region SVNEnumMaps
        private static Dictionary<string, ESVNFileStatus> SVNFileStatusMap = new Dictionary<string, ESVNFileStatus>()
        {
            {"normal", ESVNFileStatus.Normal},
            {"added", ESVNFileStatus.Added},
            {"conflicted", ESVNFileStatus.Conflicted},
            {"deleted", ESVNFileStatus.Deleted},
            {"ignored", ESVNFileStatus.Ignored},
            {"modified", ESVNFileStatus.Modified},
            {"replaced", ESVNFileStatus.Replaced},
            {"unversioned", ESVNFileStatus.Unversioned},
            {"missing", ESVNFileStatus.Missing},
        };

        private static Dictionary<string, ESVNProperty> SVNPropertyMap = new Dictionary<string, ESVNProperty>()
        {
            {"none", ESVNProperty.None},
            {"normal", ESVNProperty.Normal}            
        };
        #endregion

        #region Varible declaration

        private bool mNeedCleanUp = false;
        private ICommandLineOperation svnCurrentAssetsOperation;
        private object svnStatusDatabaseLock = new object();
        private Dictionary<string, SVNStatus> mSvnStatusDatabase = new Dictionary<string, SVNStatus>();
        private Dictionary<string, SVNStatus> svnStatusDatabase
        {
            get
            {
                lock (svnStatusDatabaseLock)
                {
                    return mSvnStatusDatabase;
                }
            }
        }

        public readonly AsyncCommandLineOperation asyncSvnStatusOperation;
        public readonly SyncCommandLineOperation syncSvnStatusOperation;
        public readonly SyncCommandLineOperation syncLocalSvnStatusOperation;

        public delegate void SVNDatabaseHandler(object source, SVNDatabaseArgs e);
        public event SVNDatabaseHandler SVNDatabaseUpdated;

        #endregion

        #region Private methods

        private void OnDatabaseHandlerEvent(SVNDatabaseArgs e)
        {
            if (SVNDatabaseUpdated != null) SVNDatabaseUpdated(this, e);
        }

        private void EditorUpdate()
        {
            if (!IsBusy() && mNeedCleanUp)
            {
                SVNCleanUp();
                mNeedCleanUp = false;
            }
        }

        private bool IsBusy()
        {
            return (svnCurrentAssetsOperation != null && svnCurrentAssetsOperation.isBusy);
        }

        private bool ParseStatusResult(XmlDocument xmlDoc)
        {
            if (!xmlDoc.HasChildNodes) return false;

            XmlNodeList entries = xmlDoc.GetElementsByTagName("entry");

            foreach (XmlNode entryIt in entries)
            {
                SVNStatus svnStatus = new SVNStatus();
                string assetPath = ("Assets/" + entryIt.Attributes["path"].InnerText.Replace('\\', '/')).Trim();

                XmlElement repos_status = entryIt["repos-status"];
                if (repos_status != null)
                {
                    svnStatus.reflectionLevel = ESVNReflectionLevel.Repository;
                    XmlElement lockStatus = repos_status["lock"];
                    if (lockStatus != null)
                    {
                        if (lockStatus["owner"] != null) svnStatus.owner = lockStatus["owner"].InnerText;
                        svnStatus.lockStatus = ESVNLockStatus.LockedOther;
                    }
                }

                XmlElement wc_status = entryIt["wc-status"];
                if (wc_status != null)
                {
                    if (wc_status.Attributes["item"] == null || !SVNFileStatusMap.TryGetValue(wc_status.Attributes["item"].InnerText, out svnStatus.fileStatus)) continue;
                    if (wc_status.Attributes["props"] == null || !SVNPropertyMap.TryGetValue(wc_status.Attributes["props"].InnerText, out svnStatus.property)) { }

                    if (svnStatus.reflectionLevel == ESVNReflectionLevel.None) svnStatus.reflectionLevel = ESVNReflectionLevel.Local;
                    if (wc_status.Attributes["revision"] != null) svnStatus.revision = Int32.Parse(wc_status.Attributes["revision"].InnerText);
                    if (wc_status.Attributes["wc-locked"] != null && wc_status.Attributes["wc-locked"].InnerText == "true") mNeedCleanUp = true;
                    if (wc_status.Attributes["tree-conflicted"] != null) svnStatus.treeConflictStatus = (wc_status.Attributes["tree-conflicted"].InnerText == "true") ? ESVNTreeConflictStatus.TreeConflict : ESVNTreeConflictStatus.Normal;

                    XmlElement commit = wc_status["commit"];
                    if (commit != null)
                    {
                        if (commit.Attributes["revision"] != null) svnStatus.lastModifiedRevision = Int32.Parse(commit.Attributes["revision"].InnerText);
                        if (commit["author"] != null) svnStatus.user = commit["author"].InnerText;
                    }

                    XmlElement lockStatus = wc_status["lock"];
                    if (lockStatus != null)
                    {
                        if (lockStatus["owner"] != null) svnStatus.owner = lockStatus["owner"].InnerText;
                        svnStatus.lockStatus = ESVNLockStatus.LockedHere;
                    }
                }

                if (svnStatus.fileStatus == ESVNFileStatus.Modified && svnStatus.lockStatus != ESVNLockStatus.LockedHere && svnStatus.property != ESVNProperty.None)
                {
                    svnStatus.bypassRevisionControl = true;
                }

                if (svnStatus.reflectionLevel != ESVNReflectionLevel.None)
                {
                    lock (svnStatusDatabaseLock)
                    {
                        mSvnStatusDatabase[assetPath] = svnStatus;
                    }
                }

            }
            //Debug.Log("SVN Database updated. Number of elements: [" + m_svnStatusMap.Count + "]");

            return true;

        }

        private void SendSVNErrorEvent(CommandLineOutput output)
        {
            if (output.failed)
            {
                OnDatabaseHandlerEvent(new SVNDatabaseErrorArgs(output));
            }
        }

        #endregion

        public void SVNCleanUp()
        {
            Debug.Log("SVNCleanUp");
            EditorUtility.UnloadUnusedAssets();
            ExecuteSVNAction(new CommandLineOperation[]{
                CreateSVNAction("cleanup", true),                
                syncLocalSvnStatusOperation
            });
        }

        public void SVNBypassRevision(bool shouldByPass, string[] assets)
        {
            foreach (string assetPathIt in assets)
            {
                if (svnStatusDatabase.ContainsKey(assetPathIt))
                {
                    svnStatusDatabase[assetPathIt].bypassRevisionControl = shouldByPass;
                }
            }
        }

        public void SVNBypassRevision(string[] assets)
        {
            SVNBypassRevision(true, assets);
        }

        public CommandLineOperation CreateSVNAction(string action)
        {
            return CreateSVNAction(action, true);
        }

        public CommandLineOperation CreateSVNAction(string action, bool asynchronous)
        {
            return CreateSVNAction(action, asynchronous, new string[] { });
        }

        public CommandLineOperation CreateSVNAction(string action, bool asynchronous, string[] assets)
        {
            return CreateSVNAction(action, asynchronous, assets, (object source, CommandLineCompletedEventArgs e) =>
            {                
                if (asynchronous)
                {
                    SyncedToUpdateOperation syncOp = new SyncedToUpdateOperation((object context) =>
                    {
                        if (e.output.failed)
                        {
                            SendSVNErrorEvent(e.output);
                        }
                        OnDatabaseHandlerEvent(new SVNDatabaseUpdatedArgs(e.output, new Dictionary<string, SVNStatus>(svnStatusDatabase)));
                    }, null);
                    syncOp.Start();
                }
                else
                {
                    if (e.output.failed)
                    {
                        SendSVNErrorEvent(e.output);
                    }
                    OnDatabaseHandlerEvent(new SVNDatabaseUpdatedArgs(e.output, new Dictionary<string, SVNStatus>(svnStatusDatabase)));
                }
            });
        }

        public CommandLineOperation CreateSVNAction(string action, bool asynchronous, string[] assets, CommandLine.ExecutionFinishedHandler finishedHandler)
        {
            string workingDirectory = Application.dataPath + "\\..\\";
            string arguments = action;
            if (assets.Length > 0)
            {
                arguments += " \"" + assets.Aggregate((i, j) => i + "\" \"" + j) + "\"";
            }
            CommandLineOperation operation;
            if (asynchronous)
            {
                operation = new AsyncCommandLineOperation(new CommandLine("svn", "--non-interactive " + arguments, workingDirectory), finishedHandler);
            }
            else
            {
                operation = new SyncCommandLineOperation(new CommandLine("svn", "--non-interactive " + arguments, workingDirectory), finishedHandler);
            }

            return operation;
        }

        public void ExecuteSVNAction(ICommandLineOperation operation)
        {
            ExecuteSVNAction(new ICommandLineOperation[] { operation });
        }
        public void ExecuteSVNAction(ICommandLineOperation[] operations)
        {
            if (!EditorApplication.isPlaying)
            {
                if (!IsBusy())
                {
                    Queue<ICommandLineOperation> commandQueue = new Queue<ICommandLineOperation>();
                    foreach (ICommandLineOperation operationIt in operations)
                    {
                        commandQueue.Enqueue(operationIt);
                    }
                    QueuedCommandLineOperation commandQueueOperation = new QueuedCommandLineOperation(commandQueue);
                    svnCurrentAssetsOperation = commandQueueOperation;
                    OnDatabaseHandlerEvent(new SVNDatabaseBusyArgs(svnCurrentAssetsOperation));
                    svnCurrentAssetsOperation.Execute();
                }
                else
                {
                    string request = "";
                    foreach (CommandLineOperation operationIt in operations)
                    {
                        request += operationIt.ToString() + "\n";
                    }
                    Debug.Log("Busy with: \n" + svnCurrentAssetsOperation.ToString() + "\n\nOperation canceled: \n" + request);
                }
            }
            else
            {
                Debug.Log("SVN Action ignored during play");
            }

        }


    }
}

#endif